package com.panopset.flywheel;

import static com.panopset.Util.x;
import static com.panopset.centralen.UniqueKey.generateStackHashKey;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.panopset.MapProvider;
import com.panopset.RezIO;
import com.panopset.StringLineSupplier;
import com.panopset.Strings;
import com.panopset.Util;
import com.panopset.UtilIO;
import com.panopset.io.FileProcessor;

/**
 * Panopset Flywheel. <h3>Commands</h3> There are 7 commands that you may use in
 * a Flywheel template. Commands have a default syntax that starts with
 * <b>${&#064;</b>, followed by the letter associated with the command, followed
 * by a space, followed by a parameter, followed by the default closing syntax
 * of <b>}</b>. <h5>f - File</h5>
 *
 * <pre>
 * ${&#064f somefile.txt}
 * </pre>
 *
 * Output to the specified file, until the matching ${&#064q} is found. <h5>p -
 * Push</h5>
 *
 * <pre>
 *
 * ${&#064p variableName}
 * </pre>
 *
 * Everything following this command is pushed into a String buffer, until a q
 * command is reached. <h5>l - List</h5>
 *
 * <pre>
 * ${&#064l someListFile.txt} or ${&#064l someFile.txt&tilde;,}
 * </pre>
 *
 * Read the given file, and for each line execute the template from this list
 * command up until its matching q command. If no <b>token</b> is defined as a
 * variable, then each line will be stored in variable <b>1</b>. If there is a
 * token defined after a ~, the line will be split by that token, and stored in
 * variables named after integers, in order.
 *
 * <h5>q - Quit</h5>
 *
 * <pre>
 * ${&#064q}
 * </pre>
 * <ul>
 * <li>If the q command follows a p command, the String buffer is defined as the
 * variable name that was provided in the p command.</li>
 * <li>If the q command follows an l command, the String buffer is read for the
 * each item in the list.</li>
 * </ul>
 * <h5>t - Template</h5>
 *
 * <pre>
 * ${&#064t someTemplateFile.txt}
 * </pre>
 *
 * Continue execution using the supplied template script file. <h5>Variable</h5>
 * There is no command associated with a variable, so you drop the <b>&#064</b>
 * directive indicator, and then you specify a variable just as you would in any
 * ant script or unix shell. The variable must have been defined either in a map
 * provided to the script through Script.Builder.mergeMap, or a Push command.
 *
 * <pre>
 * ${variableName}
 * </pre>
 *
 * <h5>e - Execute</h5> Excecute any static method. Parameters may be variable
 * names only. Example:
 *
 * <pre>
 *
 * ${&#064p name}panopset${q}
 *
 * ${&#064e com.panopset.Strings.capitalize(name)}
 *
 * </pre>
 *
 * The above script will output:
 *
 * <pre>
 *
 * Panopset
 *
 * </pre>
 *
 * @author Karl Dinwiddie
 */
public final class Flywheel implements MapProvider {

    /**
     * Target directory.
     */
    private File targetDirectory;

    /**
     * @return Target directory.
     */
    public File getTargetDirectory() {
        return targetDirectory;
    }

    /**
     * File.
     */
    private File file;

    /**
     * String line supplier.
     */
    private final StringLineSupplier sls;

    /**
     * Currently resolving command.
     */
    private Command resolvingCommand;

    /**
     * Used for debugging.
     *
     * @return Currently resolving command.
     */
    public Command getResolvingCommand() {
        return resolvingCommand;
    }

    /**
     * Writer for this Flywheel.
     */
    private StringWriter pwriter;

    /**
     * Map stack.
     */
    private final Stack<NamedMap<String, String>> mapStack

    = new Stack<NamedMap<String, String>>();

    /**
     * @return Map stack.
     */
    Stack<NamedMap<String, String>> getMapStack() {
        return mapStack;
    }

    /**
     * @return All map values.
     */
    public Map<String, String> getAllValues() {
        Map<String, String> rtn = new HashMap<String, String>();
        for (NamedMap<String, String> nm : getMapStack()) {
            for (Entry<String, String> e : nm.getMap().entrySet()) {
                rtn.put(e.getKey(), e.getValue());
            }
        }
        return rtn;
    }

    /**
     * Current command file.
     */
    private CommandFile currentCommandFile;

    /**
     * @return Current command file.
     */
    CommandFile getCurrentCommandFile() {
        return currentCommandFile;
    }

    /**
     * Set current command file.
     *
     * @param commandFile
     *            New current command file.
     */
    void setCurrentCommandFile(final CommandFile commandFile) {
        currentCommandFile = commandFile;
    }

    /**
     * Replacements.
     */
    private List<String[]> replacements;

    /**
     * Registered objects.
     */
    private Map<String, Object> registeredObjs;

    /**
     * Report.
     */
    private Report report;

    /**
     * Set report.
     *
     * @param newReport
     *            New report.
     */
    void setReport(final Report newReport) {
        report = newReport;
    }

    /**
     * @param args
     *            <pre>
     *     [0] = script file name, args[1] = target directory name.
     *       or
     *     [0] = properties file name where these properties are required:
     *            com.panopset.flywheel.script=&lt;your script file name&gt;
     * com.panopset.flywheel.target=&lt;your target file name&gt;
     * </pre>
     */
    public static void main(final String... args) {
        try {
            if (args == null || args.length == 0 || args.length > 2) {
                System.out.println("Params are script and target directory.");
                // new Gui().setVisible(true);
            } else {
                Flywheel flywheel = null;
                if (args.length == 1) {
                    flywheel = new Flywheel.Builder().properties(
                            new File(args[0])).construct();
                } else {
                    flywheel = new Flywheel.Builder().file(new File(args[0]))
                            .targetDirectory(new File(args[1])).construct();
                }
                flywheel.exec();
            }
        } catch (RuntimeException ex) {
            Util.log(ex);
        }
    }

    /**
     * Copy package resource.
     *
     * @param resourcePath
     *            Java source path resource.
     * @param targetPath
     *            Target path, relative to Flywheel target directory.
     * @throws IOException
     *             IO Exception.
     */
    public void copyPackageResource(final String resourcePath,
            final String targetPath) throws IOException {
        try {
            com.panopset.Util.copyLibraryResource(resourcePath,
                    RezIO.getCanonicalPath(targetDirectory) + "/" + targetPath);
        } catch (IOException ex) {
            StringWriter sw = new StringWriter();

            sw.append("baseDirectoryPath: " + baseDirectoryPath);
            sw.append(Strings.getEol());
            sw.append("Template file: " + RezIO.getCanonicalPath(file));
            sw.append(Strings.getEol());
            sw.append("targetDirectory: " + targetDirectory);
            sw.append(Strings.getEol());
            sw.append("resourcePath: " + resourcePath);
            sw.append(Strings.getEol());
            sw.append("targetPath: " + targetPath);
            sw.append(Strings.getEol());
            if (template != null && template.getStringLineSupplier() != null) {
                sw.append("executing from: "
                        + template.getStringLineSupplier().getFullName());
                sw.append(Strings.getEol());
                sw.append("Line: ");
                sw.append("" + template.getStringLineSupplier().getLine());
                sw.append(Strings.getEol());
            }
            Util.log(sw.toString());
            throw (ex);
        }
    }

    /**
     * Flywheel constructor.
     *
     * @param stringLineSupplier
     *            line supplier.
     */
    private Flywheel(final StringLineSupplier stringLineSupplier) {
        sls = stringLineSupplier;
        this.mapStack
                .push(new NamedMap<String, String>(generateStackHashKey()));
        this.mapStack.peek().put(ReservedWords.FILE, sls.getName());
    }

    /**
     * Is target directory valid.
     *
     * @return true if a valid, writable target directory has been specified.
     */
    public boolean isTargetDirectoryValid() {
        if (targetDirectory != null && !targetDirectory.exists()) {
            UtilIO.mkdirs(targetDirectory);
        }
        return targetDirectory != null && targetDirectory.isDirectory()
                && targetDirectory.exists() && targetDirectory.canWrite();
    }

    /**
     * This method should only be used when a writer was specified in the
     * builder. In other words, when you are using the templates to generate a
     * text String, instead of files, then you would use this method to get the
     * output.
     *
     * @return StringWriter holds script output.
     */
    public StringWriter getWriter() {
        if (pwriter == null) {
            pwriter = new StringWriter();
        }
        return pwriter;
    }

    /**
     * @param writer
     *            Writer.
     */
    public void setWriter(final StringWriter writer) {
        pwriter = writer;
    }

    /**
     * Put map value, that will be available to future v commands.
     *
     * @param key
     *            Variable name.
     * @param value
     *            Variable value.
     */
    public void put(final String key, final String value) {
        mapStack.peek().put(key, value);
    }

    /**
     * Load properties.
     *
     * @param relativePath
     *            Path is relative to the parent of the main Flywheel script
     *            file.
     * @return empty String.
     */
    public String loadProperties(final String relativePath) {
        File f = new File(getBaseDirectoryPath() + "/" + relativePath);
        Properties p = new Properties();
        if (!f.exists()) {
            try {
                Util.copyLibraryResource(relativePath, f);
            } catch (IOException ex) {
                Util.log("relativePath: " + relativePath);
                Logger.getLogger(Flywheel.class.getName()).log(Level.SEVERE,
                        null, ex);
            }
        }
        Util.loadProperties(p, f);
        for (Object k : p.keySet()) {
            put(k.toString(), p.getProperty(k.toString()).toString());
        }
        return "";
    }

    /**
     * Suppress replacements.
     *
     * <b>Usage</b>
     *
     * <pre>
     * ${&#064p foo}bar{&#064q}
     * ${&#064e script.suppressReplacements(true)}
     * A: ${&#064v foo}
     * ${&#064e script.suppressReplacements(false)}
     * B: ${&#064v foo}
     * </pre>
     *
     * <b>Output</b>
     *
     * <pre>
     *
     * A:
     * B: bar
     *
     * </pre>
     *
     * @param value
     *            If true, all v commands will be not output anything.
     */
    public void suppressReplacements(final String value) {
        if (value == null) {
            control.setReplacementsSuppressed(false);
            return;
        }
        control.setReplacementsSuppressed(Boolean.parseBoolean(value));
    }

    /**
     * Get map value.
     *
     * @param key
     *            Variable name.
     * @return value or null if key not found.
     */
    @Override
    public String get(final String key) {
        if (mapStack.isEmpty()) {
            stop("mapStack empty, this should be an impossible condition.");
            return null;
        }
        Stack<NamedMap<String, String>> stack

        = new Stack<NamedMap<String, String>>();
        for (NamedMap<String, String> item : mapStack) {
            stack.push(item);
        }
        NamedMap<String, String> m = stack.pop();
        while (true) {
            String rtn = m.get(key);
            if (rtn != null) {
                return rtn;
            }
            if (stack.isEmpty()) {
                return null;
            }
            m = stack.pop();
        }
    }

    /**
     * Merge a map into the script map. Existing values are overwritten.
     *
     * @param map
     *            Map to merge into the script map.
     */
    public void mergeMap(final Map<String, String> map) {
        mapStack.push(

        new NamedMap<String, String>(generateStackHashKey(), map));
    }

    /**
     * Report source file.
     *
     * @param sourceFile
     *            Source file.
     */
    void reportSourceFile(final SourceFile sourceFile) {
        if (report != null) {
            report.addSourceFile(sourceFile);
        }
    }

    /**
     * Execute this script.
     *
     * @return results, for unit tests and server side operations.
     */
    public String exec() {
        String rtn = "";
        if (Util.isVerbose()) {
            Util.dspmsg("Base directory: " + getBaseDirectoryPath());
            if (file != null) {
                Util.dspmsg("Executing: " + RezIO.getCanonicalPath(file));
            }
        }
        try {
            getTemplate().output(getWriter());
            rtn = getWriter().toString();
            if (report == null) {
                return rtn;
            }
            if (file != null) {
                reportSourceFile(new SourceFile(file, getBaseDirectoryPath()));
            }
            report.exec();
        } catch (Throwable t) {
            Util.log(t);
            stop(t.getMessage());
        }
        return rtn;
    }

    /**
     * Template.
     */
    private Template template;

    /**
     * @return Template.
     */
    public Template getTemplate() {
        if (template == null) {
            template = new Template(this, sls);
        }
        return template;
    }

    /**
     * Called if something unexpected happens. Diagnostic messages are logged,
     * in addition to the message.
     *
     * @param message
     *            Log this message
     */
    public void stop(final String message) {
        Util.log(message);
        Util.log(x("Unexpected stop, dumping top map."));
        if (template != null) {
            Util.log(template.getRelativePath());
        }
        Util.dump(mapStack.peek());
        if (resolvingCommand != null) {
            Util.log(x("Stopped while executing line") + " "
                    + resolvingCommand.getLineNumber()
                    + resolvingCommand.toString());
        }
        control.stop();
    }

    /**
     * Used by debuggers.
     *
     * @return true if the stop method was called for any reason, usually some
     *         error condition.
     */
    public boolean isStopped() {
        return control.isStopped();
    }

    /**
     * Base directory path.
     */
    private String baseDirectoryPath;

    /**
     * Get the directory in which the script top template file resides. This is
     * used as a relative path base for all subsequent template files created
     * with the template command: <b>${&#064;t templateRelativePathName}</b>.
     *
     * @return Full path of parent directory of the top script template file.
     */
    public String getBaseDirectoryPath() {
        if (baseDirectoryPath == null) {
            baseDirectoryPath = System.getProperty("user.home");
        }
        return baseDirectoryPath;
    }

    /**
     * Set replacements vector.
     *
     * @param replacementsList
     *            Replacements vector.
     */
    void setReplacements(final List<String[]> replacementsList) {
        this.replacements = replacementsList;
    }

    /**
     * @return Replacements vector.
     */
    protected List<String[]> getReplacements() {
        if (replacements == null) {
            replacements = new ArrayList<String[]>();
        }
        return replacements;
    }

    /**
     * Append simple file name to getBaseDirectoryPath.
     *
     * @param simpleFileName
     *            Simple file name (no path).
     * @return Full relative path of simpleFileName
     */
    public String getBaseRelativePath(final String simpleFileName) {
        return getBaseDirectoryPath() + "/" + simpleFileName;
    }

    /**
     * Flywheel builder.
     */
    public static final class Builder {

        /**
         * Is output enabled.
         */
        private boolean isOutputEnabled = true;

        /**
         * Writer.
         */
        private StringWriter writer;

        /**
         * Optional Flywheel script report.
         */
        private Report pReport;

        /**
         * Target directory.
         */
        private File targetDirectory;

        /**
         * Template array.
         */
        private String[] array;

        /**
         * String line supplier.
         */
        private StringLineSupplier sls;

        /**
         * Template file.
         */
        private File file;

        /**
         * Base directory path.
         */
        private String baseDirectoryPath;

        /**
         * Variables map.
         */
        private Map<String, String> map = new HashMap<String, String>();

        /**
         * Registered objects for static methods that return String values.
         */
        private Map<String, Object> registeredObjects

        = new HashMap<String, Object>();

        /**
         * @return Optional Flywheel script report.
         */
        Report getReport() {
            if (pReport == null) {
                pReport = new Report();
            }
            return pReport;
        }

        /**
         * Create a script. Example usage:
         *
         * <pre>
         * new Script.Builder()
         *
         * .targetDirectory(&quot;publish/site&quot;).scriptFile(
         *
         * &quot;templates/index.txt&quot;).properties(
         *
         * &quot;my.properties&quot;) // Script
         *         // variables.
         *         .constructScript().exec();
         * </pre>
         *
         * @return Panopset Flywheel Script object.
         */
        public Flywheel construct() {
            String scriptFileName = this.map.get(ReservedWords.SCRIPT);
            String targetDirName = this.map.get(ReservedWords.TARGET);
            if (Strings.isPopulated(scriptFileName)) {
                this.file = new File(scriptFileName);
            }
            if (this.file == null) {
                if (this.array == null) {
                    this.file = new File("com.panopset.flywheel.EmptyFile.txt");
                    this.sls = new StringLineSupplier(this.file);
                } else {
                    this.sls = new StringLineSupplier(this.array);
                }
            } else {
                this.sls = new StringLineSupplier(this.file);
            }
            Flywheel flywheel = new Flywheel(sls);
            if (this.baseDirectoryPath != null) {
                flywheel.baseDirectoryPath = this.baseDirectoryPath;
            }
            if (this.file != null && flywheel.baseDirectoryPath == null) {
                flywheel.baseDirectoryPath = Util.getParentDirectory(this.file);
            }
            flywheel.mergeMap(this.map);
            flywheel.setReplacements(replacements);
            if (Strings.isPopulated(targetDirName)) {
                flywheel.targetDirectory = new File(targetDirName);
            }
            if (flywheel.targetDirectory == null) {
                flywheel.targetDirectory = getTargetDirectory();
            }
            flywheel.put(ReservedWords.TARGET,
                    RezIO.getCanonicalPath(getTargetDirectory()));
            flywheel.setWriter(writer);
            registeredObjects.put(ReservedWords.FLYWHEEL, flywheel);
            flywheel.setRegisteredObjects(registeredObjects);
            if (this.file != null) {
                flywheel.file = this.file;
            }
            if (pReport != null) {
                pReport.setFlywheel(flywheel);
            }
            flywheel.setOutputEnabled(isOutputEnabled);
            return flywheel;
        }

        /**
         * Set target directory. This is where your output is going. Directory
         * structures will be created as needed, and <b>existing files are wiped
         * without warning</b>.
         *
         * @param newTargetDirectory
         *            All output files specified by the ${&#064f fileName}
         *            command are relative paths based here.
         * @return Builder.
         */
        public Builder targetDirectory(final File newTargetDirectory) {
            targetDirectory = newTargetDirectory;
            return this;
        }

        /**
         * Set script file.
         *
         * @param scriptFile
         *            Specify a single controlling script file.
         * @return Builder.
         */
        public Builder file(final File scriptFile) {
            file = scriptFile;
            return this;
        }

        /**
         * You do not need to specify this if you are specifying a File in
         * Flywheel.Builder.file, as the parent directory of the main template
         * file would be used. This method would rarely be needed, but is
         * available for flexibility.
         *
         * @param path
         *            Base directory path.
         * @return Builder.
         */
        public Builder baseDirectoryPath(final String path) {
            baseDirectoryPath = path;
            return this;
        }

        /**
         * Place html reports on your template structure in this directory. Use
         * reports(File reportsDirectory) if this is for the server side.
         *
         * @param reportsRelativePath
         *            Relative path to targetDirectory
         * @return Builder.
         */
        public Builder reports(final String reportsRelativePath) {
            getReport().setReportsRelativePath(reportsRelativePath);
            return this;
        }

        /**
         * If you want reports placed in a directory outside the target
         * directory, or if there is no target directory, as would be the case
         * on the server side, you may specify a reports directory here. An
         * error will be thrown if the reportsDirectory file exists and is not a
         * directory.
         *
         * @param newReportsDirectory
         *            Reports directory.
         * @return Builder.
         * @throws IOException
         *             IO Exception.
         */
        public Builder reports(final File newReportsDirectory)
                throws IOException {
            if (newReportsDirectory.exists() && newReportsDirectory.isFile()) {
                throw new IOException("Will not overwrite file "
                        + RezIO.getCanonicalPath(newReportsDirectory));
            }
            getReport().setReportDirectory(newReportsDirectory);
            return this;
        }

        /**
         * If you supply a String array here, do not use the <b>file</b>
         * Flywheel.Builder method.
         *
         * @param newStringArray
         *            Script commands in String array.
         * @return Builder.
         */
        public Builder input(final String[] newStringArray) {
            array = newStringArray;
            return this;
        }

        /**
         * If you supply a List of Strings here, do not use the <b>file</b>
         * Flywheel.Builder method.
         *
         * @param newInput
         *            Script commands in String List.
         * @return Builder.
         */
        public Builder input(final List<String> newInput) {
            final String[] sa = new String[newInput.size()];
            int i = 0;
            for (String s : newInput) {
                sa[i++] = s;
            }
            return this.input(sa);
        }

        /**
         * Defaults to &quot;temp&quot;.
         *
         * @return target directory File.
         */
        private File getTargetDirectory() {
            if (targetDirectory == null) {
                return new File("temp");
            }
            return targetDirectory;
        }

        /**
         * You may pre-define variables by supplying a map. Please remember to
         * stay away from the the numbers, as they are used in lists.
         *
         * @param additionalMap
         *            Additional map. In the case of duplicate keys, the most
         *            recently added map takes precedence.
         * @return Builder.
         */
        public Builder map(final Map<String, String> additionalMap) {
            com.panopset.Util.copyMap(additionalMap, map);
            return this;
        }

        /**
         * Add a key value pair to the map.
         *
         * @param key
         *            Variable command name.
         * @param value
         *            Variable value.
         * @return Builder.
         */
        public Builder map(final String key, final String value) {
            this.map.put(key, value);
            return this;
        }

        /**
         * Specify list line tokens, default is none ("").
         *
         * @param tokens
         *            List line tokens.
         * @return Builder.
         */
        public Builder tokens(final String tokens) {
            map.put(ReservedWords.TOKENS, tokens);
            return this;
        }

        /**
         * Register any instantiated objects you want available to the Execute
         * command here. You will be able to use any method of those objects
         * that accept 0 or more String parameters, and that return a single
         * String result. Recommended, but not required, to qualify objects with
         * their path names. com.panopset.flywheel.Flywheel is registered
         * automatically.
         *
         * @param key
         *            Name object will be known by, in Execute commands.
         * @param obj
         *            Instance Execute commands will invoke methods on.
         * @return Builder.
         */
        public Builder registerObject(final String key, final Object obj) {
            registeredObjects.put(key, obj);
            return this;
        }

        /**
         * Load properties file into pre-defined variables.
         *
         * @see Flywheel.Builder#map
         * @param propertiesFile
         *            Properties file.
         * @return Builder.
         */
        public Builder properties(final File propertiesFile) {
            if (propertiesFile != null && propertiesFile.exists()) {
                return map(Util.loadPropsFromFile(propertiesFile));
            }
            if (propertiesFile != null) {
                Util.log(RezIO.getCanonicalPath(propertiesFile) + " "
                        + x("does not exist, skipping."));
            }
            return this;
        }

        /**
         * @param properties
         *            Properties file.
         * @return Builder.
         */
        public Builder properties(final Properties properties) {
            return map(Util.loadMapFromProperties(properties));
        }

        /**
         * Define replacements in a file specified by the replacements
         * parameter. First line of that file contains a single character that
         * is the separator for subsequent lines that define replacements.
         *
         * @param replacementsFile
         *            file.
         * @return Builder.
         */
        public Builder replacements(final File replacementsFile) {
            final List<String[]> v = new ArrayList<String[]>();
            new FileProcessor.Builder()
                    .fileAndListener(replacementsFile,
                            new FileProcessor.Listener() {

                                private String separator;

                                @Override
                                public void finish(

                                final FileProcessor processor) {
                                    // nothing to do here
                                }

                                @Override
                                public void process(
                                        final FileProcessor processor,
                                        final String[] s) {
                                    if (separator == null) {
                                        separator = s[0];
                                    } else {
                                        StringTokenizer st

                                        = new StringTokenizer(s[0], separator);
                                        v.add(new String[] {st.nextToken(),
                                                st.nextToken() });
                                    }
                                }
                            }).construct().exec();
            for (String[] r : v) {
                replacement(r);
            }
            return this;
        }

        /**
         * Add a global replacement. Replacements are performed in the order
         * added.
         *
         * @param s
         *            s[0] = from s[1] = to.
         * @return Builder.
         */
        public Builder replacement(final String[] s) {
            return replacement(s[0], s[1]);
        }

        /**
         * Add a global replacement. Replacements are performed in the order
         * added.
         *
         * @param from
         *            String to replace with the to String.
         * @param to
         *            String to put in place of the from String.
         * @return Builder.
         */
        public Builder replacement(final String from, final String to) {
            getReplacements().add(new String[] {from, to });
            return this;
        }

        /**
         * Replacements map.
         */
        private List<String[]> replacements;

        /**
         * @return replacements map.
         */
        private List<String[]> getReplacements() {
            if (replacements == null) {
                replacements = new ArrayList<String[]>();
            }
            return replacements;
        }

        /**
         * If you specify a writer, you need not specify a targetDirectory, as
         * it would be meaningless, and any File commands are ignored, as you
         * are intending your output to be a StringWriter.
         *
         * @param newWriter
         *            StringWriter to write output to.
         * @return Builder.
         */
        public Builder writer(final StringWriter newWriter) {
            writer = newWriter;
            isOutputEnabled = false;
            return this;
        }

        /**
         * If you want a link back to your site at the top of your reports,
         * supply a title and link here.
         *
         * @param title
         *            Usually "home".
         * @param href
         *            Relative or absolute path. For example ("../index.html").
         *            Don't forget the correct number of "../" entries, based
         *            upon what you had put in the reports path.
         * @see com.panopset.flywheel.Flywheel.Builder#preports
         * @return Builder.
         */
        public Builder reportsHomeLink(final String title, final String href) {
            getReport().setReportsHomeLinkTitle(title);
            getReport().setReportsHomeLinkHref(href);
            return this;
        }
    }

    /**
     * Is replacements repressed.
     *
     * @return true if replacements suppressed flag is set.
     */
    public boolean isReplacementsSuppressed() {
        return getControl().isReplacementsSuppressed();
    }

    /**
     * @param registeredObjects
     *            Registered objects.
     */
    public void setRegisteredObjects(

    final Map<String, Object> registeredObjects) {
        for (Entry<String, Object> e : registeredObjects.entrySet()) {
            getRegisteredObjects().put(e.getKey(), e.getValue());
        }
    }

    /**
     * @return Registered objects for execute commands.
     */
    public Map<String, Object> getRegisteredObjects() {
        if (registeredObjs == null) {
            registeredObjs = new HashMap<String, Object>();
        }
        return registeredObjs;
    }

    /**
     * Flywheel control.
     */
    private final Control control = new Control();

    /**
     * Get control.
     *
     * @return Flywheel Control.
     */
    public Control getControl() {
        return control;
    }

    /**
     * Is output to files enabled.
     */
    private boolean isOutputEnabled = true;

    /**
     * @return True iff output to files is enabled, default is true.
     */
    public boolean isOutputEnabled() {
        return isOutputEnabled;
    }

    /**
     * Set whether or not Flywheel will write output to text files. Default is
     * true, you might want to set to false if all you are doing is String
     * manipulation in memory.
     *
     * @param value
     *            Value to set.
     */
    public void setOutputEnabled(final boolean value) {
        this.isOutputEnabled = value;
    }
}
